import selectors
import socket,copy
import types,struct
import time
from threading import Timer,Thread
import public_lib
public_lib.run_path(__file__)
HOST = '0.0.0.0'    # 服务器地址
PORT = 2404         # 服务器端口
ADDR = 1            # 地址
MAX_INFO_REAL = 30  # 实数信息最大帧长度
MAX_INFO_INT = 40   # 整数信息最大帧长度
MAX_INFO_COIL = 60  # 线圈信息最大帧长度
IOA = 0             # IOA地址索引
VALUE = 1           # 值索引
CONNECT_NUM = 10    # 最大连接数
COMMAND_SET = [45,46,47,48,49,50,51,58,59,60,61,62,63,64,110,111,112,113,
               101,102,103,104,105,106,105,0x7A]
S_WD_ID = 3         # S帧写数据ID索引
S_ADDR = 0          # S帧地址索引
S_VALUE = 1         # S帧值索引
LOGGER_FILE = 'iec104_Server_log.txt'   # 日志文件名
def cmp_dict(new_dict,last_dict):
    '''
    比较新字典与上次字典，返回差异字典
    '''
    # 列表比较
    def cmp_list(list1,list2):
        list_tmp = []   # 临时列表
        # 两个列表的的值不一致时，把不一致的项添加到临时列表中
        if len(list1) == len(list2):
            for i in range(len(list1)):
                if list1[i][VALUE] != list2[i][VALUE]:
                    list_tmp.append(list1[i])
        return list_tmp
    tmp_dict = {}        # 临时字典
    tmp_list = []        # 临时列表
    tmp_list_all = []    # 临时列表
    # 遍历字典的键值
    for key in new_dict:
        for num in range(len(new_dict[key])):
            tmp_list = cmp_list(new_dict[key][num],last_dict[key][num])
            tmp_list_all.append(tmp_list)
        tmp_dict[key] = tmp_list_all
        tmp_list_all = []
    return tmp_dict
# 累加设定值
def acc(value,ac=2):
    '''
    默认收发序号累加2
    '''
    value += ac
    if value >0xffff:
        value = 0
    return value
# 按指定长度分割列表
def chunk_list(lst, chunk_size):
    return [lst[i:i+chunk_size] for i in range(0, len(lst), chunk_size)]
# 整数或浮点转字节串
def value2bytes(value,type_id=0x0d):
    if type_id == 0x0d:
        bytes_value = struct.pack('<f', value)
    elif type_id == 0x0b:
        bytes_value = struct.pack('<h', value)
    elif type_id == 'uint':
        bytes_value = struct.pack('<H', value)
    elif type_id == 0x01:
        bytes_value = struct.pack('<B', value)
    else:
        bytes_value =None
    return bytes_value
# n个数据转字节串
def data2bytes(data,type_id=0x0d):
    none = struct.pack('<B',0x0)
    byte_data = b''
    for i in data:
        if type_id == 0x0d or type_id == 0x0b:
            # 点位地址 + 空字节 + 值 + 空字节 ，共8个字节
            tmp = value2bytes(i[IOA],'uint') + none + value2bytes(i[VALUE],type_id) + none
        elif type_id == 0x01:
            tmp = value2bytes(i[IOA],'uint') + none + value2bytes(i[VALUE],type_id)
        byte_data += tmp
    return byte_data
# I帧处理转字节串
def apdu2bytes(data,data_list,cot,type_id=0x0d):
    if data_list is None:
        return b'',data.aci_TX,data.aci_RX
    head = struct.pack('<B',0x68)   # 帧头68
    if data.first_cn and cot != 0x14:
        # 首次连接或总召唤发送序号不加1
        pass
    else:
        data.aci_TX = acc(data.aci_TX,2)
    if cot == 0x14:
        data.first_cn = False  
    seq_tx = struct.pack('<H',data.aci_TX)          # 发送序列号
    seq_rx = struct.pack('<H',data.aci_RX)          # 接收序列号
    TypeId = struct.pack('<B',type_id)              # 类型ID-0xD
    NumberOfObjects = struct.pack('<B',len(data_list))   # 对象数量
    cot = struct.pack('<H',cot)                     # 传送原因
    ca = struct.pack('<H',0x01)                     # 公共地址
    # 信息块
    info = data2bytes(data_list,type_id)
    # 信息块 = 发送序号 + 接收序号 + 类型ID + 对象数量 + 传送原因 + 公共地址 + 信息块
    info_objects =  seq_tx + seq_rx + TypeId + NumberOfObjects + cot + ca + info
    length = struct.pack('<B',len(info_objects))     # 消息长度
    return head + length + info_objects,data.aci_TX,data.aci_RX
# 下发数据处理
def rec_apdu_wd(sock,data,rx_data,caddr,addrs=None):
    tx = struct.unpack('<H',rx_data[2:4])[0]            # 主站发送序号+1，2字节无符号
    rx = struct.unpack('<H',rx_data[4:6])[0]            # 主站接收序号，2字节无符号
    addr = struct.unpack('<H',rx_data[10:12])[0]        # 公共地址，2字节无符号
    cot = struct.pack('<B',7)                           # 传输原因，按协议2字节，这边按1字节无符号转字节串     
    id = rx_data[6]                                     # 类型ID，1字节无符号
    rx_ioa = struct.unpack('<H',rx_data[12:14])[0]      # 接收IOA地址
    data.aci_TX = rx                                    # 接收序号存储器        
    data.aci_RX = tx                                    # 发送序号存储器
    data.aci_RX = acc(data.aci_RX,2)                    # 发送序号+1
    seq_tx = struct.pack('<H',data.aci_TX)              # 发送序列号，2字节无符号转字节串
    seq_rx = struct.pack('<H',data.aci_RX)              # 接收序列号，2字节无符号转字节串
    if (id == 0x32 or id == 0x2d or id == 0x31) :
        for i in addrs[id]:
            if rx_ioa == i:
                if addr == caddr :
                    apdu = rx_data[:2]+seq_tx+seq_rx+rx_data[6:8]+cot+rx_data[9:]
                    sock.send(apdu)                         # 发送确认帧数据
                    data.aci_TX = acc(data.aci_TX,2)        # 停止帧类型发送序号+1
                    seq_tx = struct.pack('<H',data.aci_TX)
                    cot = struct.pack('<B',0xa)             # 传输原因，停止0x0A
                    apdu = rx_data[:2]+seq_tx+seq_rx+rx_data[6:8]+cot+rx_data[9:]
                    sock.send(apdu)                         # 发送数据
                    if id == 0x32:
                        value = round(struct.unpack('<f',rx_data[15:19])[0],3)  # 处理浮点
                    elif id == 0x2d:
                        value = rx_data[15]                 # 处理布尔值
                    elif id == 0x31:
                        value = struct.unpack('<h',rx_data[15:17])[0]           # 处理有符号整数
                        if value > 32767:
                            value = 32767
                        elif value < -32768:
                            value = -32768
                    else:
                        value = None
                else:
                    data.aci_TX = acc(data.aci_TX,2)              # 停止帧类型发送序号+1
                    value = None
                    cot = struct.pack('<B',0x6c)                  # 停止帧类型
                    apdu = rx_data[:2]+seq_tx+seq_rx+rx_data[6:8]+cot+rx_data[9:]
                    sock.send(apdu)
                return data.aci_TX,data.aci_RX,[id,rx_ioa,value]    # 返回主站发送序号，主站接收序号，值
        else:
            # 不存在处理该指令或地址时，发送未知类型帧
            value = None
            cot = struct.pack('<B',0x6c) #传送原因，未知              
            apdu = rx_data[:2]+seq_tx+seq_rx+rx_data[6:8]+cot+rx_data[9:] 
            sock.send(apdu)
            return data.aci_TX,data.aci_RX,[id,rx_ioa,value]
    else:
        # 不存在处理该指令或地址时，发送未知类型帧
        value = None
        cot = struct.pack('<B',0x6c) #传送原因，未知              
        apdu = rx_data[:2]+seq_tx+seq_rx+rx_data[6:8]+cot+rx_data[9:] 
        sock.send(apdu)
        return data.aci_TX,data.aci_RX,[id,rx_ioa,value]
class iec104Server:
    '''
    '''
    def __init__(self,host='0.0.0.0',port=2404,data_real=[],data_int=[],data_bool=[]):
        self.selector = selectors.DefaultSelector()
        self.clients = {}           # 存储所有客户端连接 {socket: data}
        self.broadcast_timer = None # 启动定时广播
        self.ca_message = {}        # 总召唤消息
        self.message_03 = {}        # 变化上报信息对象列表
        self.addrs = {50:[],49:[],45:[]}                    # 所有IOA地址列表
        self.up_message = {0x0d:[],0x0b:[],0x01:[]}         # 上报消息
        self.download_list = []     # 下发数据[类型ID，地址，数值]
        self.last_message = {}      # 上次广播消息
        self.data_real = data_real  # 浮点数类型 ID13
        self.data_int = data_int    # 有符号整数类型 ID11
        self.data_bool = data_bool  # 布尔类型 ID1
        self.stop_flag = False      # 停止标志
        self.update_en = False
        self.setup_server(host,port)# 启动服务端
    def set_value(self,data,singlePoint):
            for num,j in enumerate(data):
                if j[S_WD_ID] == singlePoint[0] and j[S_ADDR] == singlePoint[1]:
                    data[num][S_VALUE] = singlePoint[2]
            return data
    def setup_server(self,host=HOST, port=PORT):
        """初始化服务器套接字"""
        self.server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.server_sock.bind((host, port))
        self.server_sock.listen()
        public_lib.info(LOGGER_FILE,1,f'iec104服务器状态',f'启动；监听地址：{host}，端口：{port}')
        self.server_sock.setblocking(False)
        self.selector.register(self.server_sock, selectors.EVENT_READ, data=None)
    def start_broadcast(self):
        """启动定时广播"""
        self.broadcast_message("服务器定时广播: 当前时间 " + time.strftime("%Y-%m-%d %H:%M:%S"))
        # 每5秒广播一次 (3000毫秒)
        # self.broadcast_timer = Timer(5.0, self.start_broadcast)
        # self.broadcast_timer.daemon = True  # 设置为守护线程，主程序退出时自动结束
        # self.broadcast_timer.start()
    def broadcast_message(self, message={},mode=0):
        """向所有客户端广播消息"""
        if not self.clients or self.update_en == 0:
            return
        if mode == 0:
            if self.last_message == {} :
                self.last_message = copy.deepcopy(self.message_03)
                upDate_en = 0
            else:
                self.up_message = cmp_dict(self.message_03,self.last_message)
                self.last_message = copy.deepcopy(self.message_03)
                upDate_en = 1
        elif mode == 1:
            # 模式1，总召唤数据
            self.up_message = self.message_03
            upDate_en = 1
        for sock, data in list(self.clients.items()):
            if upDate_en:
                data.update_en = 1
    def accept_connection(self, sock):
        """接受新客户端连接"""
        conn, addr = sock.accept()
        public_lib.info(LOGGER_FILE,1,f'iec104客户端连接',f' 地址：{addr[0]}，端口：{addr[1]}')
        conn.setblocking(False)
        data = types.SimpleNamespace(
            addr = addr,
            inb = b"",
            outb = b"",
            last_active=time.time(),# 上次连接时间
            aci_TX = 0,             # I帧发送计数器
            aci_RX = 0,             # I帧接收计数器
            acs_TX = 0,             # S帧发送计数器（备用）
            acs_RX = 0,             # S帧接收计数器（备用）
            update_en = 0,          # 允许数据上送
            first_cn = 1,           # 首次连接，发送序号不累加
            c_ic_en = 0,            # 总召唤使能
            broadcast_list = []     # 广播列表
        )
        events = selectors.EVENT_READ | selectors.EVENT_WRITE
        self.selector.register(conn, events, data=data)
        self.clients[conn] = data  # 添加到客户端字典
    def remove_client(self, sock):
        """移除断开连接的客户端"""
        if sock in self.clients:
            addr = self.clients[sock].addr
            public_lib.info(LOGGER_FILE,1,f'iec104客户端断开',f' 地址：{addr[0]}，端口：{addr[1]}')
            self.selector.unregister(sock)
            del self.clients[sock]
            sock.close()
    def service_connection(self, key, mask):
        """处理客户端连接的数据收发"""
        sock = key.fileobj
        data = key.data
        if mask & selectors.EVENT_READ:
            try:
                recv_data = sock.recv(1024)
                if recv_data:
                    # 更新最后活动时间
                    data.inb = recv_data
                    # 传输确认
                    if data.inb.hex() == '680407000000' :
                        # 发送输出缓冲区中的数据
                        sock.send(bytes.fromhex('68040b000000'))
                    # 测试链路帧
                    if data.inb.hex() == '680443000000' :
                        # 发送输出缓冲区中的数据
                        sock.send(bytes.fromhex('680483000000'))
                    # 总召唤确认帧
                    if len(recv_data)>6 and recv_data[0] == 0x68 and recv_data[6] == 0x64 and recv_data[8] == 0x06:
                        tx = struct.unpack('<H',recv_data[2:4])[0]         # 主站发送序号+1，存prx
                        rx = struct.unpack('<H',recv_data[4:6])[0]         # 主站接收序号
                        paddr = struct.unpack('<H',recv_data[10:12])[0]    # 公共地址
                        seq_tx = struct.pack('<H',rx)
                        code = struct.pack('<B',7)                         # 确认帧类型
                        data.aci_TX = rx
                        data.aci_RX = tx
                        data.aci_RX = acc(data.aci_RX,2)
                        seq_rx = struct.pack('<H',data.aci_RX)
                        apdu = recv_data[:2]+seq_tx+seq_rx+recv_data[6:8]+code+recv_data[9:]
                        sock.send(apdu)
                        # 公共地址等于从站设定地址或FFFF时，上送数据
                        if (ADDR == paddr or paddr == 0xffff) and self.ca_message is not None:
                            data.c_ic_en = 1
                            try:
                                for type_id in [0x0d,0x0b,0x01]:
                                    for sinfo in self.ca_message[type_id]:
                                        apdu,data.aci_TX,data.aci_RX = apdu2bytes(data,sinfo,cot=0x14,type_id=type_id)
                                        sock.send(apdu)
                            except Exception as e:
                                public_lib.info(LOGGER_FILE,0,f'iec104总召唤上送数据出错',e)
                            pass
                        if (ADDR != paddr and paddr != 0xffff) or data.c_ic_en == 1:
                            data.c_ic_en = 0
                            code = struct.pack('<B',10)                  # 停止帧类型
                            data.aci_TX = acc(data.aci_TX,2)
                            seq_tx = struct.pack('<H',data.aci_TX)
                            apdu = recv_data[:2]+seq_tx+seq_rx+recv_data[6:8]+code+recv_data[9:]
                            sock.send(apdu)
                    try:    
                        if len(recv_data)>6 and recv_data[0] == 0x68 and (recv_data[6] in COMMAND_SET) and len(recv_data)>12:
                            data.acs_TX,data.acs_RX,list_downLoad = rec_apdu_wd(sock,data,recv_data,ADDR,self.addrs)
                            if data.first_cn:
                                data.first_cn = 0
                            if list_downLoad[2] != None:
                                self.download_list.append(list_downLoad)
                    except Exception as e:
                        public_lib.info(LOGGER_FILE,0,'iec104处理APDU出错',e)
                        self.remove_client(sock)
                else:
                    # 客户端断开连接
                    self.remove_client(sock)
                    return
            except (ConnectionError, OSError) as e:
                self.remove_client(sock)
        if data.update_en :
            try:
                # 遥测上送
                for type_id in [0x0d,0x0b,0x01]:
                    for single_data in self.up_message[type_id]:
                        if single_data != []:
                            data.outb,data.aci_TX,data.aci_RX = apdu2bytes(data,single_data,cot=0x03,type_id=type_id)
                            sock.send(data.outb)
                            if data.first_cn:
                                data.first_cn = 0
                data.update_en = 0
            except (ConnectionError, OSError) as e:
                public_lib.info(LOGGER_FILE,0,f'iec104发送数据到 {data.addr}出错',e)
                self.remove_client(sock)
    def get_addr(self):   
        list_code = [50,49,45]
        list_data = [self.data_real,self.data_int,self.data_bool]
        for num,i in enumerate(list_code):
            try:
                self.addrs[i] = [j[0] for j in list_data[num]]
            except Exception as e:
                self.addrs[i] = []
    def run(self):
        """运行服务器主循环"""
        try:
            get_addr_en = 0
            while True:
                if self.stop_flag == True:
                    break
                events = self.selector.select(timeout=None)
                for key, mask in events:
                    if key.data is None:
                        self.accept_connection(key.fileobj)
                    else:
                        self.service_connection(key, mask)
                self.message_03[13] = chunk_list(self.data_real,MAX_INFO_REAL) # 根据类型ID的最大信息长度进行分段
                self.message_03[1] = chunk_list(self.data_bool,MAX_INFO_COIL)
                self.message_03[11] = chunk_list(self.data_int,MAX_INFO_INT)
                self.ca_message = self.message_03
                self.update_en = not (self.data_real==[] and self.data_int==[] and self.data_bool==[]) # 允许数据上送
                if self.update_en :
                    self.get_addr()
                self.broadcast_message() # 变化上送
                # 处理下发数据，实际应用时需要注释掉,根据需求重写[类型ID，地址，值]
                # if self.download_list != []:
                #     continue
                    # for i in self.download_list:
                    #     self.data_real = self.set_value(self.data_real,i)
                    #     self.data_bool = self.set_value(self.data_bool,i)
                    #     self.data_int = self.set_value(self.data_int,i)
                    # self.download_list = []
                time.sleep(0.05)
                # 这里可以添加其他定期检查逻辑
                # 例如: 检查不活跃的客户端并断开
        except KeyboardInterrupt as e:
            pass
            # public_lib.info(LOGGER_FILE,1,f'关闭服务器',e)
        finally:
            # 清理资源
            if self.broadcast_timer:
                self.broadcast_timer.cancel()
            for sock in list(self.clients.keys()):
                self.remove_client(sock)
            self.selector.unregister(self.server_sock)
            self.server_sock.close()
            self.selector.close()
            public_lib.info(LOGGER_FILE,1,f'iec104服务器状态','已关闭')
def main(server):
    # ---模拟数据---
    ac0 = 0
    while True:
        # ---模拟数据---
        data_real = [[i,round(random.random()*100,2),0x0d,50,'点位标识符'] for i in range(1,102)]  # [地址，值,类型ID,写入ID,点位标识符] 浮点
        data_bool = [[i,random.randint(0,1),0x01,45,'点位标识符'] for i in range(1,102)]           # [地址，值,类型ID,写入ID,点位标识符] 线圈
        data_int = [[i,random.randint(-32768,32767),0x0b,49,'点位标识符'] for i in range(1,102)]   # [地址，值,类型ID,写入ID,点位标识符] 整型
        if server.clients and ac0 >= 5:
            server.data_real = data_real
            # server.data_bool = data_bool
            # server.data_int = data_int
            # server.stop_flag = True
            # break
            ac0 = 0
        ac0 += 1
        time.sleep(1)
def server_run(host,port):
    server = iec104Server(host,port)
    server.run()
if __name__ == "__main__":
    import random
    # ---模拟数据---
    data_real = [[i,0.0,0x0d,50,'点位标识符'] for i in range(1,102)]  # [地址，值,类型ID,写入ID,点位标识符] 浮点
    data_bool = [[i,0,0x01,45,'点位标识符'] for i in range(1,102)]    # [地址，值,类型ID,写入ID,点位标识符] 线圈
    data_int = [[i,0,0x0b,49,'点位标识符'] for i in range(1,102)]     # [地址，值,类型ID,写入ID,点位标识符] 整型
    data_real = []  
    data_bool = []
    data_int = []
    # server_run('0.0.0.0',2404)
    server = iec104Server(host='0.0.0.0',port=2404,
                            data_real=data_real,
                            data_int=data_int,
                            data_bool=data_bool)
    
    gb = Thread(target=main,args=(server,))
    gb.daemon = True
    gb.start()
    server.run()